package mviewer;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.ButtonGroup;
import javax.swing.JToggleButton;
import javax.swing.JCheckBox;
import javax.swing.JSeparator;
import javax.swing.JMenuBar;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JLabel;
import javax.swing.JSlider;
import javax.swing.BoxLayout;
import java.awt.event.KeyEvent;
import java.awt.BorderLayout;
import java.awt.Component;
import javax.swing.JButton;
import java.io.FileNotFoundException;
import java.io.IOException;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseEvent;
import java.awt.Point;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.MouseWheelEvent;
import java.awt.Dimension;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFileChooser;
import mviewer.functions.Rotation;
import mviewer.functions.Scaling;
import mviewer.functions.Translation;
import mviewer.functions.IFunction;
import mviewer.renderer.IRenderer;
import mviewer.model3dLoader.ModelLoaderFacade;
import mviewer.model3dLoader.ObjModelLoader;
import mviewer.model3dLoader.PlgModelLoader;
import mviewer.renderer.software.SoftwareRenderer;
import mviewer.types.Camera;
import mviewer.types.IModel3d;
import mviewer.types.IVector;
import mviewer.types.IWritableVector;
import mviewer.types.Transformation;
import mviewer.types.VectorData;

public class MViewer extends JFrame {

	static private final int INITIAL_SELECTED_RENDERER_INDEX = 0;

	static private final String TRANSLATION = "Translation";
	static private final String ROTATION = "Rotation";
	static private final String SCALING = "Scaling";

	static private final Logger logger = Logger.getLogger(MViewer.class.getName());

	static public void main(String[] args) throws ClassNotFoundException {
		new MViewer();
	}

	private JMenuBar menuBar;
	private JMenu mData, mRenderer;
	private JMenuItem mOpen;
	private JPanel viewPanel, pCtrl, pMeta, pTrans, pLeft, pRight;
	private JPanel[] pCoord = new JPanel[3];
	private JButton bCenter, bReset;
	private JCheckBox cIso, cWireframe;
	private ButtonGroup gAction = new ButtonGroup();
	private JToggleButton bMove, bRotate, bScale;
	private JSlider[] slider = new JSlider[3];
	private JLabel[] lCoord = new JLabel[3];
	private JFileChooser fc = new JFileChooser();
	private boolean adjusting = false;
	private ModelLoaderFacade modelLoader;
	private IRenderer renderer;
	private Component modelView;
	private IModel3d model;
	private String transformationName = TRANSLATION;
	private Transformation transformation = new Transformation();
	private Map<String, IFunction> transformationValueFunctions = new HashMap<String, IFunction>();
	private Camera camera = new Camera(new VectorData(0f, 0f, 50f), 0.1f, 500f);

	public MViewer() throws ClassNotFoundException {
		super("MViewer");
		this.setDefaultCloseOperation(EXIT_ON_CLOSE);

		/*
		 * INITIALIZE MODEL
		 */
		transformationValueFunctions.put(TRANSLATION, new Translation());
		transformationValueFunctions.put(ROTATION, new Rotation());
		transformationValueFunctions.put(SCALING, new Scaling());
		modelLoader = new ModelLoaderFacade();
		modelLoader.addLoader(new PlgModelLoader());
		modelLoader.addLoader(new ObjModelLoader());


		/*
		 * INITIALIZE VIEW
		 */
		setLayout(new BorderLayout());
		// initialize menu
		setJMenuBar(menuBar = new JMenuBar());
		menuBar.add(mData = new JMenu("Model"));
		mData.setMnemonic(KeyEvent.VK_M);
		// add open button
		mData.add(mOpen = new JMenuItem("öffnen", KeyEvent.VK_O));
		mOpen.addActionListener(new OpenListener());
		// add renderer selection
		menuBar.add(mRenderer = new JMenu("Renderer"));
		mRenderer.setMnemonic(KeyEvent.VK_R);
		mRenderer.getAccessibleContext().setAccessibleDescription("Rendererauswahl");
		ButtonGroup rg = new ButtonGroup();
		JRadioButtonMenuItem[] mRendererList = new JRadioButtonMenuItem[] {
				new JRadioButtonMenuItem("jogl"),
				new JRadioButtonMenuItem("software")
		};
		RendererSelectionListener rl = new RendererSelectionListener();
		for (JRadioButtonMenuItem mi : mRendererList) {
			mRenderer.add(mi);
			mi.addActionListener(rl);
			rg.add(mi);
		}
		mRendererList[INITIAL_SELECTED_RENDERER_INDEX].setSelected(true);
		mRenderer.add(new JSeparator());
		JCheckBox jcb;
		mRenderer.add(cWireframe = new JCheckBox("Wireframe"));
		cWireframe.addActionListener(new RenderingWireframeListener());
		mRenderer.add(jcb = new JCheckBox("ZBuffer"));
		jcb.addActionListener(new RenderingZBufferListener());

		viewPanel = new JPanel();
//		viewPanel.setBackground(Color.BLACK);
		getContentPane().add(viewPanel, BorderLayout.CENTER);
		viewPanel.setLayout(new BorderLayout());

		// initialize control panel
		add(pCtrl = new JPanel(), BorderLayout.PAGE_END);
		pCtrl.add(pLeft = new JPanel(), BorderLayout.WEST);
		pCtrl.add(pRight = new JPanel(), BorderLayout.EAST);
		pCtrl.add(cIso = new JCheckBox("ISO"));
		pLeft.setLayout(new BoxLayout(pLeft, BoxLayout.Y_AXIS));
		pRight.setLayout(new BoxLayout(pRight, BoxLayout.Y_AXIS));
		pLeft.add(pTrans = new JPanel());
		pLeft.add(pMeta = new JPanel());
		pMeta.add(bCenter = new JButton("center"));
		pMeta.add(bReset = new JButton("reset"));
		pTrans.add(bMove = new JToggleButton(TRANSLATION, true));
		pTrans.add(bRotate = new JToggleButton(ROTATION));
		pTrans.add(bScale = new JToggleButton(SCALING));
		gAction.add(bMove);
		gAction.add(bRotate);
		gAction.add(bScale);
		bMove.setActionCommand(TRANSLATION);
		bRotate.setActionCommand(ROTATION);
		bScale.setActionCommand(SCALING);
		ActionListener al = new SelectorListener();
		bMove.addActionListener(al);
		bRotate.addActionListener(al);
		bScale.addActionListener(al);

		String[] labels = new String[] {"x","y","z"};
		SliderListener sl = new SliderListener();
		for (int i=0; i<3; i++) {
			pRight.add(pCoord[i] = new JPanel());
			pCoord[i].add(new JLabel(labels[i]));
			pCoord[i].add(slider[i] = new JSlider());
			slider[i].setMinimumSize(new Dimension(360, 50));
			slider[i].setName(labels[i]);
			slider[i].setValue(0);
			slider[i].setMinimum(-1000);
			slider[i].setMaximum(1000);
			slider[i].addChangeListener(sl);
			pCoord[i].add(lCoord[i] = new JLabel("0"));
		}

		// add button listener
		bCenter.addActionListener(new CenterListener());// center button listener
		bReset.addActionListener(new ResetListener());	// register reset listener
		setVisible(true);
		pack();
		setSize(800, 650);
		setExtendedState(getExtendedState() | MAXIMIZED_BOTH);

		updateSliderRange();
		Class<? extends IRenderer> rendererClass = retrieveRendererClass(mRendererList[INITIAL_SELECTED_RENDERER_INDEX].getText());
		initRenderer(rendererClass);
	}

	private void initRenderer(Class<? extends IRenderer> clazz) {
		if (renderer == null || renderer.getClass() != clazz) {
			try {
				if (renderer != null) {
					modelView.setVisible(false);
					viewPanel.setVisible(false);
					viewPanel.remove(modelView);
				}
				renderer = (IRenderer) clazz.newInstance();
				modelView = renderer.createComponent();
				renderer.setCamera(camera);
				// add mouse listener
				modelView.addMouseMotionListener(new MotionListener());
				modelView.addMouseWheelListener(new ZListener());
				viewPanel.add(modelView, BorderLayout.CENTER);
				modelView.setVisible(true);
				viewPanel.setVisible(true);
				if (model != null)
					displayModel(model);
			} catch (InstantiationException ex) {
				Logger.getLogger(MViewer.class.getName()).log(Level.SEVERE, null, ex);
			} catch (IllegalAccessException ex) {
				Logger.getLogger(MViewer.class.getName()).log(Level.SEVERE, null, ex);
			}
		}
	}

	private void displayModel(IModel3d model) {
		this.model = model;
		transformation = model.getTransformation();
		renderer.setModel(model);
		renderer.display();
		updateSliderPosition();
	}

	private Class<? extends IRenderer> retrieveRendererClass(String name)
			throws ClassNotFoundException {
		name = String.format("mviewer.renderer.%s.%sRenderer", name,
					name.substring(0, 1).toUpperCase() + name.substring(1));
		return (Class<? extends IRenderer>)Class.forName(name);
	}

	private void updateSliderRange() {
		IFunction f = transformationValueFunctions.get(transformationName);
		adjusting = true;
		// set slider minimum
		for (JSlider s : slider)
			s.setMinimum(f.getMinimum());
		// set slider maximum
		for (JSlider s : slider)
			s.setMaximum(f.getMaximum());
		adjusting = false;
		updateSliderPosition();
	}

	private void updateSliderPosition() {
		if (model==null)
			return;
		IFunction fn = transformationValueFunctions.get(transformationName);
		IWritableVector v = getCurrentTransformation();
		adjusting = true;
		slider[0].setValue(fn.renderX(v.getX()));
		slider[1].setValue(fn.renderX(v.getY()));
		slider[2].setValue(fn.renderX(v.getZ()));
		adjusting = false;
		updateTransformationLabels();
	}

	private void updateTransformationLabels() {
		IWritableVector v = getCurrentTransformation();
		lCoord[0].setText(String.format("%.1f", v.getX()));
		lCoord[1].setText(String.format("%.1f", v.getY()));
		lCoord[2].setText(String.format("%.1f", v.getZ()));
	}

	/*private void updateShader() {
		try {
			IShader shader = selectedShader.newInstance();
			if (selectedShaderDecorator!=null)
				shader = selectedShaderDecorator.getDeclaredConstructor(IShader.class).newInstance(shader);
//			renderer.setShader(shader);
		} catch (InstantiationException e) {
			logger.log(Level.SEVERE, "Couldn't instantiate shader", e);
		} catch (IllegalAccessException e) {
			logger.log(Level.SEVERE, "Couldn't access shader", e);
		} catch (Exception e) {
			Logger.getLogger(MViewer.class.getName()).log(Level.SEVERE, null, e);
		}
	}*/

	private IWritableVector getCurrentTransformation() {
		try {
			return (VectorData) transformation.getClass().getMethod("get" + transformationName)
					.invoke(transformation);
		} catch(Exception e) {
			throw new UnsupportedOperationException("Couldn't access transformation vector: " + transformationName, e);
		}
	}

	private void sliderMove(char axis, int value) {
		if (model == null)
			return;
		IFunction fn = transformationValueFunctions.get(transformationName);
		float val = fn.renderY(value);
		IWritableVector v = getCurrentTransformation();
		if (cIso.isSelected()) {
			v.setX(val);
			v.setY(val);
			v.setZ(val);
			updateSliderPosition();
		} else {
			switch (axis) {
				// transformation on single axis
				case 'x':
					v.setX(val);
					break;
				case 'y':
					v.setY(val);
					break;
				case 'z':
					v.setZ(val);
					break;
				default:
					throw new IllegalArgumentException("Unsupported axis: " + axis);
			}
		}
		updateTransformationLabels();
		renderer.display();
	}

	private void mouseMove(IVector move) {
		if (model==null)
			return;
		IWritableVector v = getCurrentTransformation();
		if (cIso.isSelected()) {
			float val = v.getY() + move.getY();
			v.setX(val);
			v.setY(val);
			v.setZ(val);
		} else {
			float x = v.getX() + move.getX(),
				  y = v.getY() + move.getY(),
				  z = v.getZ() + move.getZ();
			v.setX(x);
			v.setY(y);
			v.setZ(z);
		}
		updateSliderPosition();
		renderer.display();
	}


	/*
	 * LISTENER
	 */

	private class OpenListener implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			int n = JFileChooser.CANCEL_OPTION;
			if (e.getSource()==mOpen)
				n = fc.showOpenDialog(MViewer.this);
			if (n==JFileChooser.APPROVE_OPTION) {
				try {
					displayModel(modelLoader.loadModel(fc.getSelectedFile().getAbsolutePath()));
				} catch(FileNotFoundException ex) {
				} catch(IOException ex) {}
			}
		}
	}

	private class CenterListener implements ActionListener {
		public void actionPerformed(ActionEvent e) {
		}
	}

	private class ResetListener implements ActionListener {
		public void actionPerformed(ActionEvent e) {

		}
	}

	private class SelectorListener implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			transformationName = e.getActionCommand();
			updateSliderRange();
		}
	}

	private class SliderListener implements ChangeListener {
		public void stateChanged(ChangeEvent e) {
			if (adjusting==false) {
				JSlider src = (JSlider)e.getSource();
				sliderMove(src.getName().charAt(0), src.getValue());
			}
		}
	}

	
	// define mouse listener
	private class MotionListener implements MouseMotionListener {
		private Point last;
		public void mouseDragged(MouseEvent e) {
			Point d = renderMoveVal(e);
			int x = 0, y = 0, z = 0;
			if (transformationName.equals(ROTATION)) {
				y = -d.x;
				x = -d.y;
			} else {
				x = -d.x;
				y = d.y;
			}
			mouseMove(new VectorData(x, y, z));
		}
		public void mouseMoved(MouseEvent e) {
			last = e.getPoint();
		}
		private Point renderMoveVal(MouseEvent e) {
			Point p = e.getPoint()
				, res = last==null
					? new Point(0,0)
					: new Point(last.x-p.x, last.y-p.y);
			last = p;
			return res;
		}
	}

	private class ZListener implements MouseWheelListener {
		public void mouseWheelMoved(MouseWheelEvent e) {
			int val = e.getWheelRotation() * 3;
			if (val != 0)
				mouseMove(new VectorData(0, 0, val));
		}
	}


	private class RendererSelectionListener implements ActionListener {

		public void actionPerformed(ActionEvent e) {
			if (model!=null) {
				String rendererName = ((JRadioButtonMenuItem)e.getSource()).getText();
				try {
					Class<? extends IRenderer> rendererClass = retrieveRendererClass(rendererName);
					initRenderer(rendererClass);
				} catch (ClassNotFoundException ex) {
					logger.severe("renderer not found: " + rendererName);
				}
			}
		}
	}

	private class RenderingWireframeListener implements ActionListener {
		public void actionPerformed(ActionEvent ae) {
			JCheckBox chckbx = (JCheckBox)ae.getSource();
			try {
				((SoftwareRenderer) renderer).setWireframe(chckbx.isSelected());
				renderer.display();
			} catch(ClassCastException e) {
				chckbx.setSelected(false);
			}
		}
	}

	private class RenderingZBufferListener implements ActionListener {
		public void actionPerformed(ActionEvent ae) {
			JCheckBox chckbx = (JCheckBox)ae.getSource();
			try {
				((SoftwareRenderer) renderer).setOutputBufferImage(chckbx.isSelected());
				renderer.display();
			} catch(ClassCastException e) {
				chckbx.setSelected(false);
			}
		}
	}

}